﻿using System.Text.RegularExpressions;

namespace Bundler
{
    using System;
    using System.Collections.Concurrent;
    using System.Collections.Generic;
    using System.Data;
    using System.Text;
    using System.Web;

    public static class BundleTable
    {
        #region Constants and Fields

        private const string CssExtension = ".css";
        private const string JsExtenion = ".js";
        private static ConcurrentDictionary<int, string> _aliases;
        private static ConcurrentDictionary<string, BundleResponse> _responses;

        #endregion

        #region Public Properties

        public static ConcurrentDictionary<string, BundleResponse> Responses
        {
            get { return _responses; }
        }

        #endregion

        #region Public Methods and Operators

        public static void Add(Bundle bundle)
        {
            if(bundle == null)
                throw new ArgumentNullException("bundle");

            var response = bundle.Options.EnableLazyOptimization ? new BundleResponse {Bundle = bundle} : Process(bundle);

            if (_responses == null)
            {
                _responses = new ConcurrentDictionary<string, BundleResponse>();
            }

            _responses.AddOrUpdate(response.Bundle.VirtualPath, response, (k, v) => response);

            if (!bundle.Options.DisableOldIeCompatability && typeof(CssMinifyImageEmbed) == bundle.BundleType)
            {
                var bundleOldIe = new Bundle(string.Format("{0}-ie", bundle.VirtualPath), typeof(CssMinify));
                foreach (var item in bundle.Items)
                {
                    switch (item.Type)
                    {
                        case BundleContentSourceType.VirtualPath:
                            bundleOldIe.AddFile(item.Value);
                            break;
                        case BundleContentSourceType.RawContent:
                            bundleOldIe.AddContent(item.Value);
                            break;
                    }
                }

                var responseOldIe = bundle.Options.EnableLazyOptimization ? new BundleResponse { Bundle = bundleOldIe } : Process(bundleOldIe);
                _responses.AddOrUpdate(bundleOldIe.VirtualPath, responseOldIe, (k, v) => responseOldIe);
            }
        }

        public static string ResolveUrl(string virtualPath)
        {
            if(virtualPath == null)
                throw new ArgumentNullException("virtualPath");

            BundleResponse response;
            if (Responses != null && Responses.TryGetValue(virtualPath, out response))
            {
                if (!response.Processed)
                    Process(response);

                var extension = string.Empty;
                if(response.Bundle.Options != null && response.Bundle.Options.IncludeFileExtensionInAlias)
                {
                    if (typeof(JsMinify).IsAssignableFrom(response.Bundle.BundleType))
                    {
                        extension = JsExtenion;
                    }
                    else if (typeof(CssMinify).IsAssignableFrom(response.Bundle.BundleType))
                    {
                        extension = CssExtension;
                    }
                }

                virtualPath = string.Format("~/{0}{1}", response.Alias, extension);
            }

            return HttpHelper.ToAbsolute(virtualPath);
        }

        public static string CompositeUrl(Type bundleType, params string[] virutalPaths)
        {
            if(bundleType == null)
                throw new ArgumentNullException("bundleType");

            if(virutalPaths == null || virutalPaths.Length == 0)
                throw new ArgumentNullException("virutalPaths");

            var compositeUrl = String.Format("~/composite_{0}_{1}", bundleType, String.Join(",", virutalPaths));
            if (Responses == null || !Responses.ContainsKey(compositeUrl))
            {
                var compositeBundle = new Bundle(compositeUrl, bundleType);
                foreach (var virutalPath in virutalPaths)
                {
                    compositeBundle.AddFile(virutalPath);
                }
                Add(compositeBundle);
            }

            return ResolveUrl(compositeUrl);
        }

        public static bool IsInTable(string virtualPath)
        {
            if (String.IsNullOrWhiteSpace(virtualPath))
            {
                throw new ArgumentNullException("virtualPath");
            }

            if (_aliases == null || Responses == null)
            {
                return false;
            }

            var path = virtualPath;
            var strKey = virtualPath.Replace("~/", String.Empty);
            int intKey;
            if (int.TryParse(strKey, out intKey))
            {
                if (_aliases.ContainsKey(intKey))
                {
                    return true;
                }
            }
            return Responses.ContainsKey(path);
        }

        public static HtmlString RenderHtmlFor(string virtualPath)
        {
            if(String.IsNullOrWhiteSpace(virtualPath)) {
                throw new ArgumentNullException("virtualPath");
            }

            const string cssElementFormat = "<link href=\"{0}\" rel=\"stylesheet\" type=\"text/css\" {1}/>";
            const string jsElementFormat = "<script src=\"{0}\" type=\"text/javascript\"></script>";
            const string cssRawFormat = "<style type=\"text/css\" {1}>{0}</style>";
            const string jsRawFormat = "<script type=\"text/javascript\">{0}</script>";
            const string jsElementFormatDefer = "<script type=\"text/javascript\">function loadBundle{0}(){{var element=document.createElement(\"script\");element.src=\"{1}\";document.body.appendChild(element);}}if(window.addEventListener){{window.addEventListener(\"load\",loadBundle{0},false);}}else if(window.attachEvent){{window.attachEvent(\"onload\",loadBundle{0});}}else{{window.onload=loadBundle{0};}}</script>";

            var response = GetResponse(virtualPath);
            if (response == null)
            {
                throw new ApplicationException(virtualPath + " not found");
            }

            var itemsToRender = new List<BundleItem>();

            if (!response.Bundle.Options.EnableOptimizations)
            {
                itemsToRender.AddRange(response.Bundle.Items);
            }
            else
            {
                itemsToRender.Add(new BundleItem
                {
                    Value = virtualPath,
                    Type = BundleContentSourceType.VirtualPath
                });
            }

            var uniqueQueryString = !response.Bundle.Options.EnableOptimizations && response.Bundle.Options.IncludeUniqueQueryStringInDebug ? String.Format("?v={0}{1:00}{2:00}{3:00}{4:00}{5:00}{6:000}", DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, DateTime.Now.Hour, DateTime.Now.Minute, DateTime.Now.Second, DateTime.Now.Millisecond) : string.Empty;

            string media = null;
            if(response.Bundle.Options != null && !String.IsNullOrWhiteSpace(response.Bundle.Options.Css.Media))
            {
                media = string.Format("media=\"{0}\"", HttpUtility.HtmlAttributeEncode(response.Bundle.Options.Css.Media.Trim()));
            }

            var sb = new StringBuilder();
            foreach (var itemToRender in itemsToRender)
            {
                switch (itemToRender.Type)
                {
                    case BundleContentSourceType.VirtualPath:
                        if (typeof(JsMinify).IsAssignableFrom(response.Bundle.BundleType))
                        {
                            var resolvedUrl = ResolveUrl(itemToRender.Value) + uniqueQueryString;
                            if (response.Bundle.Options.Js.DeferLoading)
                            {
                                sb.AppendLine(string.Format(jsElementFormatDefer, GenerateSlug(itemToRender.Value, "_"), resolvedUrl));
                            }
                            else
                            {
                                sb.AppendLine(string.Format(jsElementFormat, resolvedUrl));
                            }
                        }
                        else if (typeof(CssMinify).IsAssignableFrom(response.Bundle.BundleType))
                        {
                            sb.AppendLine(string.Format(cssElementFormat, ResolveUrl(itemToRender.Value) + uniqueQueryString, media));
                        }
                        break;
                    case BundleContentSourceType.RawContent:
                        if (typeof(JsMinify).IsAssignableFrom(response.Bundle.BundleType))
                        {
                            sb.AppendLine(string.Format(jsRawFormat, itemToRender.Value));
                        }
                        else if (typeof(CssMinify).IsAssignableFrom(response.Bundle.BundleType))
                        {
                            sb.AppendLine(string.Format(cssRawFormat, itemToRender.Value, media));
                        }
                        break;
                }
            }

            return new HtmlString(sb.ToString());
        }

        private static string GenerateSlug(string phrase, string separator)
        {
            var str = phrase.RemoveAccent().ToLower();
            str = Regex.Replace(str, @"[^a-z0-9\s-_]", string.Empty); // invalid chars
            str = Regex.Replace(str, @"\s+", " ").Trim(); // convert multiple spaces into one space
            str = str.Substring(0, str.Length <= 1024 ? str.Length : 1024).Trim(); // cut and trim it
            str = Regex.Replace(str, @"\s", separator); // separators
            str = str.Replace(separator + separator, separator);
            if (str.StartsWith(separator))
            {
                str = str.Substring(1, str.Length - 1);
            }

            if (str.EndsWith(separator))
            {
                str = str.Substring(0, str.Length - 1);
            }

            str = str.Replace(String.Format("{0}-{0}", separator), separator); // for example _-_
            str = str.Replace(String.Format("{0}_{0}", separator), separator); // for example -_-
            str = str.Replace(separator + separator, separator);
            return str;
        }

        private static string RemoveAccent(this string txt)
        {
            var bytes = Encoding.GetEncoding("Cyrillic").GetBytes(txt);
            return Encoding.ASCII.GetString(bytes);
        }

        #endregion

        #region Methods

        internal static BundleResponse GetResponse(string virtualPath)
        {
            if (virtualPath == null)
                throw new ArgumentNullException("virtualPath");

            var path = virtualPath;
            var strKey = virtualPath.Replace("~/", string.Empty);
            int intKey;
            if (int.TryParse(strKey, out intKey))
            {
                if (!_aliases.TryGetValue(intKey, out path))
                {
                    path = virtualPath;
                }
            }

            BundleResponse response;
            if (Responses != null && Responses.TryGetValue(path, out response))
            {
                if (!response.Processed)
                    Process(response);
                return response;
            }

            //it could also be an alias with an extension
            if (virtualPath.EndsWith(CssExtension, StringComparison.OrdinalIgnoreCase) || virtualPath.EndsWith(JsExtenion, StringComparison.OrdinalIgnoreCase))
            {
                strKey = virtualPath.Replace("~/", string.Empty).Replace(CssExtension, string.Empty).Replace(JsExtenion, string.Empty);
                if (int.TryParse(strKey, out intKey))
                {
                    if (_aliases.TryGetValue(intKey, out path))
                    {
                        if (Responses != null && Responses.TryGetValue(path, out response))
                        {
                            if (!response.Processed)
                                Process(response);
                            return response;
                        }
                    }
                }
            }

            return null;
        }

        internal static void Process(BundleResponse response)
        {
            if (response == null)
                throw new ArgumentNullException("response");
            if (response.Bundle == null)
                throw new NoNullAllowedException("response.Bundle");
            if(String.IsNullOrWhiteSpace(response.Bundle.VirtualPath))
                throw new NoNullAllowedException("response.Bundle.VirtualPath cannot be null or white space");

            var minifier = (IMinify)Activator.CreateInstance(response.Bundle.BundleType);
            minifier.Process(response);
            response.Processed = true;

            if(response.Content == null)
                throw new NullReferenceException("response.Content is null");

            if (_aliases == null)
            {
                _aliases = new ConcurrentDictionary<int, string>();
            }

            _aliases.AddOrUpdate(response.Alias, response.Bundle.VirtualPath, (k, v) => response.Bundle.VirtualPath);
        }

        internal static BundleResponse Process(Bundle bundle)
        {
            var response = new BundleResponse { Bundle = bundle };
            Process(response);
            return response;
        }

        #endregion
    }
}